﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using DependencyAnalysis.DataModel;

namespace DependencyAnalysis.Internal
{
    public static class LawOfDemeterPrinciple
    {
        public static IDictionary<MemberDescriptor, IEnumerable<MemberDescriptor>> SearchForConcatenatedCalls(ICollection<IDescriptor> members, InstructionCache instructionCache)
        {
            var violations = new Dictionary<MemberDescriptor, IEnumerable<MemberDescriptor>>();
            foreach (MemberDescriptor member in members)
            {
                var type = member.ImplementingType.Type;
                var instructions = instructionCache.GetInstructions(member);
                var stack = new Stack<Instruction>();
                int loads = 0;
                foreach (var instruction in instructions)
                {
                    if (instruction.OpCode == OperationCode.Castclass) continue;
                    if (IsLoadingOpCode(type, instruction))
                    {
                        loads++;
                        continue;
                    }
                    if (!IsCallingOperationCode(instruction.OpCode))
                    {
                        stack.Clear();
                        continue;
                    }
                    var method = (instruction.OperandNonSerialized as MethodBase);
                    if (method != null && method.GetParameters().Length < loads)
                    {
                        continue;
                    }
                    if (IsOperandOfFriendlyType(type, instruction.OperandNonSerialized)
                        || stack.Count == 0 && IsOperandDataObject(instruction.OperandNonSerialized))
                    {
                        continue;
                    }
                    stack.Push(instruction);
                    loads = 0;
                    if (stack.Count > 1)
                    {
                        violations.Add(member, InstructionsToMembers(stack));
                        break;
                    }
                }
            }
            return violations;
        }

        private static IEnumerable<MemberDescriptor> InstructionsToMembers(IEnumerable<Instruction> instructions)
        {
            return instructions.Select(instruction => DescriptorBuilder.BuildMemberDescriptor(instruction.OperandNonSerialized as MemberInfo));
        }

        private static readonly HashSet<OperationCode> LoadingOpCodes = new HashSet<OperationCode>{OperationCode.Ldarg,
        OperationCode.Ldarg_0,OperationCode.Ldarg_2,OperationCode.Ldarg_3, OperationCode.Ldarg_S, OperationCode.Ldarga, OperationCode.Ldarga_S,
        OperationCode.Ldc_I4, OperationCode.Ldc_I4_0, OperationCode.Ldc_I4_1, OperationCode.Ldc_I4_2, OperationCode.Ldc_I4_3, OperationCode.Ldc_I4_4,
        OperationCode.Ldc_I4_5, OperationCode.Ldc_I4_6, OperationCode.Ldc_I4_7, OperationCode.Ldc_I4_8, OperationCode.Ldc_I4_M1, OperationCode.Ldc_I4_S,
        OperationCode.Ldc_I8,OperationCode.Ldc_R4,OperationCode.Ldc_R8,OperationCode.Ldfld, OperationCode.Ldflda, OperationCode.Ldftn, OperationCode.Ldstr,
        OperationCode.Ldsfld, OperationCode.Ldsflda, OperationCode.Ldloc, OperationCode.Ldloc_0, OperationCode.Ldloc_1, OperationCode.Ldloc_2, OperationCode.Ldloc_3,
        OperationCode.Ldloc_S, OperationCode.Ldloca, OperationCode.Ldloca_S, OperationCode.Ldnull};

        private static readonly HashSet<Type> CommonTypes = new HashSet<Type>{typeof(Type), typeof(object), typeof(string), typeof(int),
        typeof(long),typeof(byte),typeof(short), typeof(ulong), typeof(uint), typeof(ushort)};

        private static bool IsLoadingOpCode(Type type, Instruction instruction)
        {
            var fieldInfo = instruction.OperandNonSerialized as FieldInfo;
            if (fieldInfo != null &&
                fieldInfo.DeclaringType != null &&
                !fieldInfo.DeclaringType.IsAssignableFrom(type))
            {
                return false;
            }
            return LoadingOpCodes.Contains(instruction.OpCode);
        }

        private static bool IsOperandOfFriendlyType(Type type, object operand)
        {
            var member = operand as MethodBase;
            if (member == null) return false;
            var declaringType = member.DeclaringType;
            return member.IsStatic || declaringType != null
                   && (declaringType.IsAssignableFrom(type) || CommonTypes.Contains(declaringType));
        }

        private static bool IsOperandDataObject(object operand)
        {
            var member = operand as MemberInfo;
            var memberDescriptor = DescriptorBuilder.BuildMemberDescriptor(member);
            return memberDescriptor.ImplementingType.IsDataObject;
        }

        private static bool IsCallingOperationCode(OperationCode opCode)
        {
            return opCode == OperationCode.Call || opCode == OperationCode.Calli ||
                opCode == OperationCode.Callvirt || opCode == OperationCode.Ldfld
                || opCode == OperationCode.Ldflda;
        }
    }
}
